package simpleftp.server;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.Callable;

import javax.swing.tree.DefaultMutableTreeNode;

import com.google.gson.Gson;

import simpleftp.ftp.Command;
import simpleftp.ftp.FTPDescriber;
import simpleftp.ftp.FileData;
import simpleftp.ftp.FileTreeNode;

/**
 * ServeurFTP is a simple package that implements a Java FTP server. With
 * ServeurFTP, you can connect to an FTP client and download multiple files. 
 */
public class FTPServer extends FTPDescriber implements Callable<Void> {
	private static final String pathStartDirectory = System.getProperty("user.dir");
	
	private int 			validations;
	private int 			portnum1, 
							portnum2;
	private StringBuilder 	directoryContent;
	private Properties 		serverProperties;
	private ServerSocket 	waitingSocket;
	private File 			ftpRootDirectory;
	private String			pathRootDirectory;
	
	/** 
	 * Initialize the server to speak with a client connected through socket s
	 * @param s
	 */
	public FTPServer(Socket s) {
		socket = s;
		validations = 0;
		NAME = "FTPServer";
		try {
			// Load server properties (allowed users)
			serverProperties = new Properties();
			serverProperties.load(new FileInputStream("users.properties"));
			try {
				reader 			= new BufferedReader(new InputStreamReader(
												socket.getInputStream()));
				writer 			= new BufferedWriter(new OutputStreamWriter(
												socket.getOutputStream()));
				sendDirectory 	= new ObjectOutputStream(
												socket.getOutputStream());
				
				sendLine("220 ");
				validations++;
				cd(pathStartDirectory);
				System.out.println("FTP Server : new client from " + socket.getInetAddress().getHostAddress());
			} catch (IOException e) {
				try {
					sendLine("error");
				} catch (IOException e1) {
					e1.printStackTrace();
				}
				e.printStackTrace();
			}
		} catch (IOException ioex) {
			System.out.println("Error: Properties File didn't load");
			try {
				sendLine("error ");
			} catch (IOException e) {
				e.printStackTrace();
			}
			ioex.printStackTrace();
		}
	}

	/**
	 * Call function : Allow connection of the client and deals with ftp commands when asked to.
	 */
	public Void call() {
		try {
			// Client connection
			
			// User name
			response = readLine();
			if (response.startsWith("USER")) {
				if (serverProperties.containsKey(response.substring(5))) {
					user = response.substring(5);
					System.out.println("Server knows user " + response);
					sendLine("331 ");
					validations++;
				} else {
					System.out.println("Server does no know user " + response);
					sendLine("error ");
				}
			}
			// User password
			response = readLine();
			if (response.startsWith("PASS")) {
				if (serverProperties.get((String) user).equals(response.substring(5))) {
					System.out.println("Server validates pass " + response);
					sendLine("230 ");
					validations++;
					// Move to the user directory
					ftpRootDirectory = new File(pathStartDirectory + System.getProperty("file.separator") + user);
					pathRootDirectory = ftpRootDirectory.getAbsolutePath();
					// Create user directory if it doesn't exist
					if(!ftpRootDirectory.exists()) {
						ftpRootDirectory.mkdirs();
					}
					File notEmpty = new File(pathRootDirectory + System.getProperty("file.separator") + ".notempty.txt");
					if(!notEmpty.exists()) {
						try {
							BufferedWriter writer = new BufferedWriter(new FileWriter(notEmpty));
							writer.write("Welcome on our ftp server.");
							writer.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				} else {
					sendLine("error ");
				}
			}

		} catch (IOException e) {
			try {
				sendLine("error ");
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
		}
		// validations == 3  <--> client connected to the server with appropriate user name and password
		while (validations == 3) {
			try {
				response = readLine();
				// Deal with FTP commands
				switch (Command.valueOf(response.split(" ")[0])) {
				
				// "print working directory"
				case PWD:
					pwd();					
					break;
				// Send complete path to working directory
				case CPWD:
					cpwd();
					break;
				// List content of current directory & send the architecture of the directory to the client
				case LIST:
					ls(false);
					break;
				// "change working directory"
				case CWD:
					cwd(null);
					break;
				// Change first working directory
				case CSD:
					csd();
					break;
				// Create a passive connection
				case PASV:
					pasv();
					break;
				// Receive a file from client
				case STOR:
					stor(null);
					break;
				// Send a file to client
				case RETR:
					retr(null, null);
					break;
				// Close connection with client
				case QUIT:
					socket.close();
					sendDirectory.close();
					System.out.println("QUIT :Goodbye");
					validations = 0;
					break;

				default:
					System.out.println("Unknown Command.");
					sendLine("error");
					break;

				}
			} catch (IOException e) {
				try {
					sendLine("error");
				} catch (IOException e1) {

					e1.printStackTrace();
				}
				e.printStackTrace();
			}

		}
		Void Void = null;
		return Void;
	}
	
	/**
	 * Send current directory name
	 */
	public String pwd() throws IOException {
		sendLine("257 " + ftpRootDirectory.getName());
		return null;
	}
	
	/**
	 * Send current directory absolute path 
	 */
	public void cpwd() throws IOException {
		sendLine("257 " + ftpRootDirectory.getAbsolutePath());
	}
	
	/**
	 * Deal connection with client to get a file
	 */
	public boolean stor(File f) {
		try {
			connectedSocket = waitingSocket.accept();
			sendLine("150 ");
			
			File toStor = new File(ftpRootDirectory.getAbsolutePath()
								+ System.getProperty("file.separator")
								+ response.substring(5));
			
			readFile(toStor, connectedSocket);
			
			waitingSocket.close();
			connectedSocket.close();
			
			sendLine("226 ");
			System.out.println("STOR " + response.split(" ")[1]
					+ " : File received");
		} catch (IOException e) {
			try {
				sendLine("error");
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
		}
		return true;
	}
	
	/**
	 * Send a file from the server to the client
	 */
	// TODO : Deal with directories download
	public boolean retr(File directory, String path) {		
		try {
			connectedSocket = waitingSocket.accept();
			sendLine("160 ");
			// Get asked filename
			String fileName = response.substring(22).replace("]", "");
			if(fileName.contains(" ")){
				String[] fileNameSplit = fileName.split(", ");
				// Skip the root directory
				fileName = fileNameSplit[1];
				// Get path to the file
				for(int i = 2 ; i<fileNameSplit.length ; i++) {
					fileName = fileName + System.getProperty("file.separator") + fileNameSplit[i];
				}
			}
			
			File file = new File(ftpRootDirectory.getAbsolutePath() + System.getProperty("file.separator") + fileName);
			if(!file.canRead()) {
				sendLine("error : cannot download " + file.getAbsolutePath());
		        waitingSocket.close();
		        connectedSocket.close();
				return false;
			}
			else if(file.isDirectory()) {
                sendLine("error : cannot download directory.");
                waitingSocket.close();
                connectedSocket.close();
                return false;
			}
			else if(!file.isFile()) {
                sendLine("error : file not found : " + file.getName());
                waitingSocket.close();
                connectedSocket.close();
                return false;
			}
			else
				sendLine("185 ");

			sendFile(file, connectedSocket);
			waitingSocket.close();
	        connectedSocket.close();
			
			sendLine("222 ");
			System.out.println("RETR " + fileName
					+ " : File sent");
		} catch (IOException e) {
			try {
				sendLine("error");
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
		}
		return true;
	}
	
	/**
	 * Send the content of current directory and a tree describing it
	 */
	public String ls(boolean b) throws IOException 
	{
		String treeSendingMethod = response.substring(5);
		boolean json = (treeSendingMethod.equals("json") ? true : false);
		directoryContent = new StringBuilder("200 ");
		String[] fileNames = ftpRootDirectory.list();
		for (String fileName : fileNames) {
			directoryContent = directoryContent.append(fileName + "|");
		}
		System.out.println(directoryContent.toString());
		sendLine(directoryContent.toString());
		sendTree(json);
		return null;
	}
	
	/**
	 * Change working directory
	 * 
	 * A description of the directory to go in is given :
	 * "[The FTP Server, parent, child1, child2]" 
	 * is an example of string sent to change working directory to child2
	 */
	public boolean cwd(String s) throws IOException {
		System.out.println("CWD");
		
		String dirPath = response.substring(21).replace("]", "");
		if(dirPath.contains(" ")){
			String[] fileNameSplit = dirPath.split(", ");
			// Skip the root directory
			dirPath = fileNameSplit[1];
			// Get path to the file
			for(int i = 2 ; i<fileNameSplit.length ; i++) {
				dirPath = dirPath + System.getProperty("file.separator") + fileNameSplit[i];
			}
		}
		
		if (dirPath.equals("home")) {
			// Go back to home directory
			cd(pathStartDirectory + System.getProperty("file.separator") + user);
			sendLine("250 ");
		}
		else if(dirPath.equals("..")) {
			// Change to parent directory
			cd(ftpRootDirectory.getParent());
			sendLine("250 ");
		}
		else {
			// Change to child directory
			File newDirectory = new File(pathRootDirectory + System.getProperty("file.separator") + dirPath);
			if(newDirectory.isDirectory()) {
				cd(newDirectory.getAbsolutePath());
				sendLine("250 ");
			}
			else {
				System.out.println("CWD cancelled : " + newDirectory.getAbsolutePath() + " is not a directory.");
				sendLine("error : " + newDirectory.getName() + " is not a directory.");
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Change working directory at the start of a session
	 * Absolute path is given by the client
	 */
	private void csd() throws IOException {
		System.out.println("CSD");
		
		if(response.substring(4) != null) {
			cd(response.substring(4));
			sendLine("260 ");
		}
		else {
			sendLine("error : cannot change directory to " + response.substring(4));
		}
	}
	
	/**
	 * Do the actual working directory change
	 * @param path The path of the directory to go in
	 */
	private void cd(String path) throws IOException {
		System.out.println("CWD new current directory:"
				+ System.getProperty("user.dir").toString());
		ftpRootDirectory = new File(path);
		pathRootDirectory = ftpRootDirectory.getAbsolutePath();
	}
	
	/**
	 * Send a serialized DefaultMutableTreeNode to the client to get him 
	 * to know the architecture of the current working directory
	 */
	private void sendTree(boolean useJson) throws IOException
	{
		if(socket == null) {
			throw new IOException("FTPServer is not connected.");
		}
		if(useJson) {
			FileTreeNode<FileData> directoryTreeNodes = new FileTreeNode<FileData>(new FileData(ftpRootDirectory));
		    createSrcNodes(directoryTreeNodes, ftpRootDirectory);
		    Gson gson = new Gson();
		    String json = gson.toJson(directoryTreeNodes);
		    sendLine("JSON " + json);
		}
		else {
			DefaultMutableTreeNode directoryTreeNodes = explore(ftpRootDirectory);
			sendDirectory.writeObject(directoryTreeNodes);
			sendDirectory.flush();
		}
	}
	
	/**
	 * Recursively create a tree from the given File
	 * @param fileTreeNode The tree to complete
	 * @param file The root of the tree
	 */
	private static void createSrcNodes(FileTreeNode<FileData> fileTreeNode, File file) {
	    if (file.isDirectory()) {
	      File[] files = file.listFiles();
	      for (int i = 0; i < files.length; i++) {
	        FileTreeNode<FileData> child = new FileTreeNode<FileData>(new FileData(files[i]));
	        createSrcNodes(child, files[i]);
	        fileTreeNode.addChild(child);
	      }
	    }
	  }
	
	/**
	 * Recursively explore the rootDirectory and create a DefaultMutableTreeNode
	 * to show its architecture
	 * @param rootDirectory The directory to explore
	 * @return The tree describing rootDirectory
	 */
	private DefaultMutableTreeNode explore(File rootDirectory)
	{
		DefaultMutableTreeNode node = new DefaultMutableTreeNode(rootDirectory.getName());
		File[] listDirectory = rootDirectory.listFiles();
		Arrays.sort(listDirectory);
		
		for(int i = 0 ; i < listDirectory.length ; i++) {
			if(listDirectory[i].canRead()) {
				if(listDirectory[i].isDirectory()) {
					DefaultMutableTreeNode subNode = explore(listDirectory[i]);
					if(subNode != null) {
						node.add(subNode);
					}
				}
				else {
					node.add(new DefaultMutableTreeNode(listDirectory[i].getName()));
				}
			}
			else {
				node = null;
			}
		}
		if (node.getChildCount() == 0) {
			node = null;
		}
		return node;
	}
	
	/**
	 * Create a passive connection
	 */
	protected void pasv() {
		try {
			waitingSocket = new ServerSocket(0);
			portnum1 = waitingSocket.getLocalPort() / 256;
			portnum2 = waitingSocket.getLocalPort() % 256;
			sendLine("227 ("
					+ InetAddress.getLocalHost().getHostAddress()
							.replace('.', ',') + "," + portnum1 + ","
					+ portnum2 + ")");
			System.out.println("PASV success");
		} catch (IOException e) {
			try {
				sendLine("error ");
				System.out.println("pasv error");
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
		}
	}
	
	public static void setDebug(boolean b) {
		DEBUG = b;
	}
}
